######################################################################################
#
# This file was provided as part of the SharePoint Mystery Deployment package
# Created by Christopher Kimbell (http://sharepointmystery.com)
#
######################################################################################

# -----------------------------------------------------------------
# Creates an object used to manipulate the Search Service
# -----------------------------------------------------------------
function New-SearchService()
{
    $obj=new-object PSObject -Property @{ 
		SearchDB = New-Database;
		CrawlDB = New-Database;
		QueryDB = New-Database;
		Pool = New-ApplicationPool;
		AdminPool = New-ApplicationPool;
		Service = New-SupportingService;
		QueryAndSiteSettingsService = New-SupportingService;
	 	Name = "Search Service";
		Proxy = "Search Service Proxy";
		ContentAccessAccount = "";
		ServiceAccount = "";
		ContactEmail = "";
		ConnectionTimeout = "180" # increased from default 30 to allow for IIS startup time
		AcknowledgementTimeout = "180" # increased from default 30 to allow for IIS startup time
		IgnoreSSLWarnings = "False";
		InternetIdentity = "";
		ProxyType = "Direct";
		PerformanceLevel = "PartlyReduced";
		IndexLocation = "";
		Partitioned = $false;
		SearchApplicationType = "Regular";
		Partitions = "1";
    }
    $obj.Service.Name = "SharePoint Server Search"
	$obj.QueryAndSiteSettingsService.Name = "Search Query and Site Settings Service"
    # -----------------------------------------------------------------
    # Validates the information in the object
    # -----------------------------------------------------------------
    Add-Method $obj "Validate" `
    {
        Write-Operation "Validating Search Service"
        $this.Pool.Validate()
        $this.Service.Validate()
        $this.SearchDB.Validate()
		$this.CrawlDB.Validate()
		$this.QueryDB.Validate()
		$this.Service.Validate()
		$this.QueryAndSiteSettingsService.Validate()
        Validate-NotEmpty "Name" $this.Name
        Validate-NotEmpty "Proxy" $this.Proxy  
		Validate-NotEmpty "IndexLocation" $this.IndexLocation 
		
		Validate-MemberOf "IgnoreSSLWarnings" $this.IgnoreSSLWarnings "True","False"
		Validate-MemberOf "ProxyType" $this.ProxyType "Direct","Proxy"
		Validate-MemberOf "PerformanceLevel" $this.PerformanceLevel "Reduced","PartlyReduced","Maximum"
		Validate-MemberOf "SearchApplicationType" $this.SearchApplicationType "Regular"," ExtendedConnector"
        
    }
    # -----------------------------------------------------------------
    # Provisions the Service
    # -----------------------------------------------------------------
    Add-Method $obj "Provision" `
    {
		# get all the account stuff out of the way at the start. 
		# They may prompt for passwords
		$pool = $this.Pool.Provision()
		$adminPool = $this.AdminPool.Provision()
				
		# the search cmdlet takes the account and password on the command line rather than a managed account
		# we create a managed account and read the password so that we don't need to store passwords in the script
		$maContentAccessAccount = Register-ManagedAccount($this.ContentAccessAccount)
		$maServiceAccount = Register-ManagedAccount($this.ServiceAccount)
		$contentPassword = Get-ManagedAccountPassword($maContentAccessAccount)		
		$servicePassword = Get-ManagedAccountPassword($maServiceAccount)	

		$searchInstance = _GetSearchServiceInstance
		$searchService = _GetSearchService
		
		Write-Operation "Configuring Enterprise Search Service"
		Set-SPEnterpriseSearchService -Identity $searchService `
			-ContactEmail $this.ContactEmail `
			-ConnectionTimeout $this.ConnectionTimeout `
			-AcknowledgementTimeout $this.AcknowledgementTimeout `
			-ProxyType $this.ProxyType `
			-IgnoreSSLWarnings $this.IgnoreSSLWarnings `
			-InternetIdentity $this.InternetIdentity `
			-PerformanceLevel $this.PerformanceLevel `
			-ServiceAccount $this.ServiceAccount `
			-ServicePassword $servicePassword  
				
		Write-Operation "Configuring Index location"
		New-SPMServerDirectory -Path $this.IndexLocation -Servers $farm.Servers
		Set-SPEnterpriseSearchServiceInstance -Identity $searchInstance -DefaultIndexLocation $this.IndexLocation
		
		#$this.SearchDB.Provision()
		#$this.CrawlDB.Provision()
		#$this.QueryDB.Provision()			
		Write-Warning "Search does not allow pre-creating databases. You have to move them after they have been created!"
		
		$sa = _CreateSearchServiceApplication $this.Name $this.SearchDB.Server $this.SearchDB.FullName $this.DB.FailoverServer $pool $adminPool $this.Partitioned $this.SearchApplicationType
		_StartSearchServiceInstance $searchInstance
		
		$adminComponent = _CreateSearchAdminComonent $sa $searchInstance
		_ConfigureContentAccessAccount $sa $this.ContentAccessAccount $contentPassword
		
		$crawlTopology = _CreateCrawlTopology $sa
		$crawlComponent = _CreateCrawlComponent $searchInstance $sa $crawlTopology $this.IndexLocation $this.CrawlDB.FullName
		$queryTopology = _CreateQueryTopology $sa $this.Partitions
		$queryComponent = _CreateQueryComponent $searchInstance $sa $queryTopology $this.IndexLocation $this.QueryDB.FullName
		
		_StartSiteAndQueryService($this.QueryAndSiteSettingsService)
		_ActivateCrawlTopology $sa $crawlTopology
		# crawl component must be running before we start the query component
		_ActivateQueryTopology $sa $queryTopology
		
		_CreateSearchServiceProxy $sa $this.Proxy $this.Partitioned
		_ConfigureSearchServiceContentSources $sa
    }

	# -----------------------------------------------------------------
    # Registers various accounts as managed
    # -----------------------------------------------------------------
	Add-Method $obj "RegisterAccounts" `
    {
		$ma = Register-ManagedAccount($this.ContentAccessAccount)
		$ma = Register-ManagedAccount($this.ServiceAccount)
    }	
    # -----------------------------------------------------------------
    # Unprovisions the Service
    # -----------------------------------------------------------------
    Add-Method $obj "Unprovision" `
    {
        Remove-ServiceApplicationProxy $this.Proxy
        Remove-ServiceApplication $this.Name
        $this.Service.Unprovision()
    }
    return $obj
}

function _GetSearchServiceInstance
{
	$searchInstance = Get-SPEnterpriseSearchServiceInstance -Local
	if($searchInstance -eq $null) {	Terminate "Could not locate Search Service Instance"}
	return $searchInstance
}

function _GetSearchService
{
	$searchService = Get-SPEnterpriseSearchService
	if($searchService -eq $null) { Terminate "Could not locate Search Service" }
	return $searchService
}

function _CreateSearchServiceApplication($displayName, $dbServer, $dbName, $failover, $pool, $adminPool, $partitioned, $type)
{
	$sa = Get-SPServiceApplication  | Where-Object {$_.DisplayName -eq $displayName}
			
	if($sa -eq $null)
	{
		Write-Operation "Creating $($displayName)"	
		$sa = New-SPEnterpriseSearchServiceApplication -Name $displayName `
			-DatabaseServer $dbServer `
			-DatabaseName $dbName `
			-FailoverDatabaseServer $failover `
			-ApplicationPool $pool `
			-AdminApplicationPool $adminPool `
			-Partitioned:$partitioned `
			-SearchApplicationType $type
	}
	else { Write-OperationDone "$($displayName) has already been provisioned" } 
	
	return $sa;
}

function _StartSearchServiceInstance($searchInstance)
{
	if ($searchInstance.Status -ne "Online") 
	{
		Write-Operation "Starting Search Service Instance"
		Start-SPEnterpriseSearchServiceInstance -Identity $searchInstance
	}
	else { Write-OperationDone "Search Service Instance was already started" }	
}
function _CreateSearchAdminComonent($sa, $searchInstance)
{
	Write-Operation "Configurating Search Administration Component"
	Set-SPEnterpriseSearchAdministrationComponent -SearchApplication $sa -SearchServiceInstance $searchInstance
	
	$adminComponent = Get-SPEnterpriseSearchAdministrationComponent -SearchApplication $sa
	if ($adminComponent.Initialized -eq $false)
	{
		Write-Operation "Initializing Search Administration Component"
		while ($adminComponent.Initialized -ne $true)
		{
			Write-OperationProgress
			Start-Sleep 1
			# get a new reference so we can check against it in the loop
			$adminComponent = Get-SPEnterpriseSearchAdministrationComponent -SearchApplication $sa
		}
	}
	else { Write-OperationDone "Search Administration Component was already initialized" }
	
	return  $adminComponent
}
function _ConfigureContentAccessAccount($sa, $account, $password)
{
	Write-Operation "Configuring Content access account"
	Set-SPEnterpriseSearchServiceApplication -Identity $sa `
		-DefaultContentAccessAccountName $account `
		-DefaultContentAccessAccountPassword $password
}

function _CreateCrawlTopology($sa)
{
	Write-Operation "Configuring Crawl Topology"
	
	$topologyWithComponents = Get-SPEnterpriseSearchCrawlTopology -SearchApplication $sa | where {$_.CrawlComponents.Count -gt 0 }
	$inactiveTopologies = Get-SPEnterpriseSearchCrawlTopology -SearchApplication $sa | where { $_.State -eq "Inactive"}
	
	# the first time around, we start with one active topology without crawl components
	# that cannot be used so we need to create a new one.
	
	# no topologies with components exist
	if($topologyWithComponents -eq $null)
	{
		# no inactive topologies, create one
		if($inactiveTopologies -eq $null)
		{
			Write-Operation "Creating Crawl Topology"
			return (New-SPEnterpriseSearchCrawlTopology -SearchApplication $sa)
		}
		else # if we have inactive topologies, use that one
		{
			# the collection doesn't implement an indexer, only IEnumerable, so we loop and get first item
			foreach($_ in $inactiveTopologies)
			{
				Write-OperationDone "Crawl Topology already exists"
				return $_
			}
		}
	}
	else # there is a topology with crawl components, use this one
	{
		# the collection doesn't implement an indexer, only IEnumerable, so we loop and get first item
		foreach($_ in $topologyWithComponents)
		{
			Write-OperationDone "Crawl Topology already exists"
			return $_
		}
	}
}
function _CreateCrawlComponent($searchInstance, $sa, $crawlTopology, $indexLocation, $crawlDBName)
{
	Write-Operation "Configuring Crawl Component"
	$crawlComponent = $crawlTopology.CrawlComponents | where {$_.ServerName -eq $env:ComputerName}
		
	if ($crawlTopology.CrawlComponents.Count -eq 0 -and $crawlComponent -eq $null) 
	{
		Write-Operation "Creating Crawl Component"
		$crawlStore = $sa.CrawlStores | where {$_.Name -eq $crawlDBName}
		
		$crawlComponent = New-SPEnterpriseSearchCrawlComponent -SearchServiceInstance $searchInstance `
			-CrawlTopology $crawlTopology `
			-CrawlDatabase $crawlStore `
			-IndexLocation $indexLocation
	} 
	else 
	{ 
		Write-OperationDone "Crawl Component already exists"	
	}
		
	return $crawlComponent
}
function _CreateQueryTopology($sa, $partitions)
{
	Write-Operation "Configuring Query Topology"
	
	$topologyWithComponents = Get-SPEnterpriseSearchQueryTopology -SearchApplication $sa | where {$_.QueryComponents.Count -gt 0 }
	$inactiveTopologies = Get-SPEnterpriseSearchQueryTopology -SearchApplication $sa | where { $_.State -eq "Inactive"}
	
	# the first time around, we start with one active topology without query components
	# that cannot be used so we need to create a new one.
	
	# no topologies with components exist
	if($topologyWithComponents -eq $null)
	{
		# no inactive topologies, create one
		if($inactiveTopologies -eq $null)
		{
			Write-Operation "Creating Query Topology"
			return (New-SPEnterpriseSearchQueryTopology -SearchApplication $sa -Partitions $partitions)
		}
		else # if we have inactive topologies, use that one
		{
			# the collection doesn't implement an indexer, only IEnumerable, so we loop and get first item
			foreach($_ in $inactiveTopologies)
			{
				Write-OperationDone "Query Topology already exists"
				return $_
			}
		}
	}
	else # there is a topology with quert components, use this one
	{
		# the collection doesn't implement an indexer, only IEnumerable, so we loop and get first item
		foreach($_ in $topologyWithComponents)
		{
			Write-OperationDone "Query Topology already exists"
			return $_
		}
	}
}
function _CreateQueryComponent($searchInstance, $sa, $queryTopology, $indexLocation, $queryDBName)
{
	$queryComponent = $queryTopology.QueryComponents | where {$_.ServerName -eq $env:ComputerName}  
	if ($queryTopology.QueryComponents.Count -eq 0 -and $queryComponent -eq $null) 
	{
		$indexPartition = Get-SPEnterpriseSearchIndexPartition -QueryTopology $queryTopology
		Write-Operation "Creating Query Component"
		$queryComponent = New-SPEnterpriseSearchQueryComponent -IndexPartition $indexPartition `
				-QueryTopology $queryTopology `
				-SearchServiceInstance $searchInstance `
				-IndexLocation $indexLocation
				
		$propertyStore = $sa.PropertyStores | where {$_.Name -eq $queryDBName}
		Set-SPEnterpriseSearchIndexPartition -Identity $indexPartition `
				-PropertyDatabase $propertyStore.Id.ToString()
	} 
	else 
	{ 
		Write-OperationDone "Query Component already exists" 
	}
	return $queryComponent
}
function _StartSiteAndQueryService($queryAndSiteSettingsService)
{
	Write-Operation "Configuring Search Query and Site Settings Service"
	$serviceType = "Microsoft.Office.Server.Search.Administration.SearchQueryAndSiteSettingsServiceInstance"
	
	$settingsService = Get-SPServiceInstance | where {$_.GetType().ToString() -eq $serviceType}
	if($settingsService -eq $null) { Terminate "Could not find Search Query and Site Settings Service" }
	if($settingsService.Status -eq "Disabled")
	{
		$queryAndSiteSettingsService.Provision()
		while($settingsService.Status -ne "Online")
		{
			Write-OperationProgress
			Start-Sleep 1
			$settingsService = Get-SPServiceInstance | where {$_.GetType().ToString() -eq $serviceType}
		}
	}
	else { Write-OperationDone "Search Query and Site Settings Service was already started" }	
		
}
function _ActivateCrawlTopology($sa, $crawlTopology)
{
	if ($crawlTopology.State -ne "Active") 
	{
		Write-Operation "Activating Crawl Topology"
		Set-SPEnterpriseSearchCrawlTopology -Identity $crawlTopology -Active -Confirm:$false
		while ($true) 
		{
			$ct = Get-SPEnterpriseSearchCrawlTopology -Identity $crawlTopology -SearchApplication $sa
			$state = $ct.CrawlComponents | where {$_.State -ne "Ready"}
			if ($ct.State -eq "Active" -and $state -eq $null) 
			{
				break
			}
			Write-OperationProgress
			Start-Sleep 1
		}	
	}
	else 
	{ 
	Write-OperationDone "Crawl Topology already active" 
	}
	# the default created has to be deleted
	$removingCrawlers = @(Get-SPEnterpriseSearchCrawlTopology -SearchApplication $sa | where {$_.State -eq "Inactive"} )
		
	if(($removingCrawlers -ne $null) -and ($removingCrawlers.Length -gt 0) )
	{
		foreach($_ in $removingCrawlers)
		{
			Remove-SPEnterpriseSearchCrawlTopology -Identity $_ -Confirm:$false
		}
	}
}
function _ActivateQueryTopology($sa, $queryTopology)
{
	if ($queryTopology.State -ne "Active") 
	{
		Write-Operation "Activating Query Topology"
		Set-SPEnterpriseSearchQueryTopology -Identity $queryTopology -Active -Confirm:$false -ErrorAction SilentlyContinue -ErrorVariable err
		
		while ($true) 
		{
			$qt = Get-SPEnterpriseSearchQueryTopology -Identity $queryTopology -SearchApplication $sa
			$state = $qt.QueryComponents | where {$_.State -ne "Ready"}
			if ($qt.State -eq "Active" -and $state -eq $null) 
			{
				break
			}
			Write-OperationProgress
			Start-Sleep 1
		}
	}
	else 
	{ 
		Write-OperationDone "Query Topology already active" 
	}
	# the default created has to be deleted
	$removingQueries = @(Get-SPEnterpriseSearchQueryTopology -SearchApplication $sa | where {$_.State -eq "Inactive"} )
	if(($removingQueries -ne $null) -and ($removingQueries.Length -gt 0))
	{
		foreach($_ in $removingQueries)
		{
			Remove-SPEnterpriseSearchQueryTopology -Identity $_ -Confirm:$false
		}
	}
}
function _CreateSearchServiceProxy($sa, $name, $partitioned)
{
	$proxy = Get-SPServiceApplicationProxy  | Where-Object {$_.DisplayName -eq $name}  
	if($proxy -eq $null)
	{
		Write-Operation "Creating Search Service proxy"
		$proxy = New-SPEnterpriseSearchServiceApplicationProxy -Name $this.Proxy `
			-SearchApplication $sa `
			-Partitioned:$partitioned
	}
	else 
	{ 
		Write-OperationDone "$($name) has already been provisioned" 
	}
	
	if ($proxy.Status -ne "Online") 
	{
		Write-Operation "Changing proxy status to Online"
		$proxy.Status = "Online"
		$proxy.Update()
	}
}
function _ConfigureSearchServiceContentSources($sa)
{
	$contentSources = @(Get-SPEnterpriseSearchCrawlContentSource -SearchApplication $sa | where {$_.Name -eq "Local SharePoint sites"})
	
	if($contentSources.Length -gt 0)
	{
		$mySiteSource = "sps3://" + $mySiteApp.DefaultZone.HostHeader
		
		if($contentSources[0].StartAddresses.Exists($mySiteSource) -eq $false)
		{
			Write-Operation "Adding $($mySiteSource) to content sources"
			$contentSources[0].StartAddresses.Add($mySiteSource)
		}
		else	
		{
			Write-OperationDone "$($mySiteSource) was already added to content source"
		}
	}	
}
